#include <memory>
#include <algorithm>
#include <stdexcept>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "Util.h"

#ifdef _WIN32

#include <windows.h>
#include <wincrypt.h>
#pragma comment(lib,"crypt32.lib")

#else

#include <openssl/md5.h>

#endif

using namespace scgi;

unsigned long long scgi::to_uint_64(std::string string_)
{
	unsigned long long result = 0;

	std::string::const_iterator it1 = string_.begin();
	while(it1 != string_.end())
	{
		unsigned char ch = *it1++;
		if(ch < '0' || '9' < ch) throw 10001;
		result = result * 10 + (ch - '0');
	}

	return result;
}

unsigned int scgi::to_uint_32(std::string string_)
{
	return static_cast<unsigned int>(to_uint_64(string_));
}

Time::Time() : time(0) { }

Time::Time(long long time_) : time(time_) { }

void Time::operator = (long long time_)
{
	time = time_;
}

long long Time::to_int_64() const
{
	return time;
}

std::string Time::to_cookie_string() const
{
	boost::posix_time::ptime ptime =
		boost::posix_time::ptime(
			boost::gregorian::date(1970,1,1))
				+ boost::posix_time::seconds(static_cast<long>(time)); // todo: year 2038 problem.

	boost::gregorian::date date = ptime.date();
	boost::posix_time::time_duration time_of_day = ptime.time_of_day();
	
	std::string result;
	result.reserve(32);
	result =
		  to_week_string(date.day_of_week())
		+ ", "
		+ to_two_string(date.day())
		+ "-"
		+ to_month_string(date.month())
		+ "-"
		+ to_four_string(date.year())
		+ " "
		+ to_two_string(time_of_day.hours())
		+ ":"
		+ to_two_string(time_of_day.minutes())
		+ ":"
		+ to_two_string(time_of_day.seconds())
		+ " GMT";

	return result;
}

std::string Time::to_http_date_string() const
{
	return "";
}

Time::operator bool () const { return time != 0; }

bool Time::operator ! () const { return time == 0; }

bool Time::operator < (Time rhs_) const { return time < rhs_.time; }

Time Time::operator + (Time rhs_) const { return time + rhs_.time; }

Time Time::operator - (Time rhs_) const { return time - rhs_.time; }

std::string Time::to_four_string(unsigned short four_)
{
	if(9999 < four_) throw 10001;

	std::string result = to_string(four_);
	while(result.size() < 4) { result = std::string(1, '0') + result; }
	return result;
}

std::string Time::to_two_string(unsigned short two_)
{
	if(59 < two_) throw 10001;

	std::string result = to_string(two_);
	if(result.size() < 2)
	{
		result.append(1, '0');
		std::reverse(result.begin(), result.end());
	}
	return result;
}

std::string Time::to_week_string(unsigned short week_)
{
	week_ -= 1;

	if(7 <= week_) throw 10001;

	char const * week_table[] = {
		"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };

	return week_table[week_];
}

std::string Time::to_month_string(unsigned short month_)
{
	month_ -= 1;

	if(12 <= month_) throw 10001;

	char const * month_table[] = {
		  "Jan", "Feb", "Mar", "Apr", "May", "Jun"
		, "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

	return month_table[month_];
}

Time Time::seconds(long seconds_)
{
	boost::posix_time::time_duration sec = boost::posix_time::seconds(seconds_);
	return static_cast<long long>(sec.total_seconds());
}

Time Time::minutes(long minutes_)
{
	return Time::seconds(minutes_ * 60);
}

Time Time::hours(long hours_)
{
	return Time::seconds(hours_ * 3600);
}

Time Time::days(long days_)
{
	return Time::seconds(days_ * 86400);
}

std::string Cookie::get(std::string key_) const
{
	const_iterator it1 = table.find(key_);
	if(it1 != table.end()) return it1->second;

	return "";
}

void Cookie::set(std::string key_, std::string value_)
{
	table[key_] = value_;
}

Cookie::const_iterator Cookie::begin() const { return table.begin(); }

Cookie::const_iterator Cookie::end() const { return table.end(); }

std::string Cookie::to_string() const
{
	std::string
		  result
		, cookie("Set-Cookie: ")
		, space(1, ' ')
		, delim(1, ';');

	const_iterator it1 = begin();
	while(it1 != table.end())
	{
		URLEncoder encode;

		result.append(cookie);
		result.append(encode(it1->first.begin(), it1->first.end()));
		result.append(1, '=');
		result.append(encode(it1->second.begin(), it1->second.end()));
		++it1;
		result.append(delim);

		if(expires)
		{
			result.append(" expires=");
			result.append(expires.to_cookie_string());
			result.append(1, ';');
		}
		if(!domain.empty()) result.append(space + "domain=" + domain + delim);
		if(!path.empty()) result.append(space + "path=" + path + delim);
		if(secure) result.append(" secure;");

		result.append("\r\n");
	}

	return std::move(result);
}

#ifdef _WIN32

std::string scgi::MD5(std::string string_)
{
	HCRYPTPROV csp_handle;
	HCRYPTHASH hash_handle;

	if(!::CryptAcquireContext(
		  &csp_handle
		, NULL
		, MS_ENHANCED_PROV
		, PROV_RSA_FULL
		, 0)) // CRYPT_VERIFYCONTEXT
	{
		 if(!::CryptAcquireContext(
			   &csp_handle
			 , NULL
			 , MS_ENHANCED_PROV
			 , PROV_RSA_FULL
			 , CRYPT_NEWKEYSET)) throw 500;
	}

	if(!::CryptCreateHash(csp_handle, CALG_MD5, 0, 0, &hash_handle)) throw 500;

	if(!::CryptHashData(
		    hash_handle
		  , reinterpret_cast<BYTE const *>(string_.c_str())
		  , string_.size()
		  , 0)) throw 500;

	DWORD digest_size = 16;
	if(!::CryptGetHashParam(
		  hash_handle
		, HP_HASHVAL
		, NULL
		, &digest_size
		, 0)) throw 500;
	std::unique_ptr<BYTE[]> digest(new BYTE[digest_size]);
	if(!::CryptGetHashParam(
		  hash_handle
		, HP_HASHVAL
		, digest.get()
		, &digest_size
		, 0)) throw 500;

	::CryptDestroyHash(hash_handle);
	::CryptReleaseContext(csp_handle, 0);

	return scgi::to_hex_string(digest.get(), digest.get() + digest_size);
}

#else

std::string scgi::MD5(std::string string_)
{
	MD5_CTX ctx;
	unsigned char digest[16];

	MD5_Init(&ctx);
	MD5_Update(
		  &ctx
		, reinterpret_cast<unsigned char const *>(string_.c_str())
		, string_.size());
	MD5_Final(digest, &ctx);

	return scgi::to_hex_string(digest, digest + 16);
}

#endif


